xtask\tasks\fuzz/
parse_fuzz_crate_toml.rs1use anyhow::Context;
8use std::collections::BTreeMap;
9use std::collections::BTreeSet;
10use std::path::Path;
11use std::path::PathBuf;
12
13#[derive(Debug)]
14pub(super) struct FuzzCrateTarget {
15 pub name: String,
16 pub allowlist: Vec<PathBuf>,
17 pub target_options: Vec<String>,
18}
19
20#[derive(Debug)]
21pub(super) struct FuzzCrateMetadata {
22 pub crate_name: String,
23 pub fuzz_dir: PathBuf,
24 pub targets: Vec<FuzzCrateTarget>,
25}
26
27#[derive(Debug)]
28pub(super) struct RepoFuzzTarget {
29 pub fuzz_dir: PathBuf,
30 #[expect(dead_code)] pub crate_name: String,
32 pub allowlist: Vec<PathBuf>,
33 pub target_options: Vec<String>,
34}
35
36fn parse_fuzz_crate_toml(cargo_toml_path: &Path) -> anyhow::Result<Option<FuzzCrateMetadata>> {
39 let manifest =
40 cargo_toml::Manifest::<super::cargo_package_metadata::PackageMetadata>::from_path_with_metadata(
41 cargo_toml_path,
42 )?;
43
44 let fuzz_meta = {
46 let validate_non_fuzz_crate_name = || {
49 let name = manifest
50 .package
51 .as_ref()
52 .map(|x| x.name())
53 .unwrap_or_default();
54
55 if name.starts_with("fuzz_") {
56 anyhow::bail!("crate '{name}' is named 'fuzz_', but isn't set up to be a fuzzer!")
57 }
58
59 anyhow::Ok(None)
60 };
61
62 let Some(metadata) = manifest.package.as_ref().and_then(|p| p.metadata.as_ref()) else {
64 return validate_non_fuzz_crate_name();
65 };
66
67 match (
70 metadata.cargo_fuzz.unwrap_or(false),
71 metadata.xtask.as_ref().and_then(|x| x.fuzz.as_ref()),
72 ) {
73 (false, None) => return validate_non_fuzz_crate_name(),
74 (true, None) | (false, Some(_)) => {
75 anyhow::bail!(
76 "`package.metadata.cargo-fuzz` must be paired with `package.metadata.xtask.fuzz`"
77 )
78 }
79 (true, Some(fuzz)) => fuzz,
80 }
81 };
82
83 if cargo_toml_path
89 .parent()
90 .and_then(|p| p.file_name())
91 .and_then(|p| p.to_str())
92 .unwrap_or_default()
93 != "fuzz"
94 {
95 anyhow::bail!("fuzzing crate Cargo.toml must be in a folder called `fuzz/`")
96 }
97
98 let fuzz_crate_name = manifest
101 .package
102 .as_ref()
103 .map(|p| p.name.as_str())
104 .unwrap_or_default();
105 let Some(fuzz_crate_name) = fuzz_crate_name.strip_prefix("fuzz_") else {
106 anyhow::bail!(r#"fuzzing crate `name` must start with "fuzz_""#)
107 };
108
109 let mut bins = BTreeSet::new();
111 for bin in manifest.bin {
112 let name = bin
113 .name
114 .context("found [[bin]] entry without explicit `name` key")?;
115
116 if bin.path.is_none() {
117 anyhow::bail!(r#"found [[bin]] entry (name = {name}) without explicit `path` key"#,)
118 }
119
120 if !(name == format!("fuzz_{fuzz_crate_name}")
123 || name.starts_with(&format!("fuzz_{fuzz_crate_name}_")))
124 {
125 anyhow::bail!(
126 r#"invalid [[bin]] entry: invalid name = "{name}". expected `name` to start with "fuzz_{fuzz_crate_name}" (i.e: "fuzz_{{crate_name}}")"#,
127 )
128 }
129
130 for (val, name) in [
131 (bin.test, "test"),
132 (bin.doctest, "doctest"),
133 (bin.doc, "doc"),
134 ] {
135 if val {
136 anyhow::bail!(r#"invalid [[bin]] entry: ensure that `{name} = false`"#)
137 }
138 }
139
140 let was_empty = bins.insert(name);
141 assert!(was_empty); }
143
144 {
146 let mut allowlists = fuzz_meta.allowlist.keys().cloned().collect::<BTreeSet<_>>();
147 for bin in bins.iter() {
148 let was_present = allowlists.remove(bin);
149 if !was_present {
150 anyhow::bail!("found [[bin]] that doesn't have an allowlist: {bin}")
151 }
152 }
153 if !allowlists.is_empty() {
154 anyhow::bail!(
155 "found allowlist entries that doesn't corresponding [[bin]] entries: {allowlists:?}"
156 )
157 }
158 }
159
160 let targets = {
161 let mut targets = Vec::new();
162 for (target_name, allowlist) in &fuzz_meta.allowlist {
163 let mut normalized_allowlist = Vec::new();
166 let mut normalized_ignorelist = BTreeSet::new();
167
168 let (allowed_globs, ignored_globs): (Vec<_>, Vec<_>) =
169 allowlist.iter().partition(|s| !s.starts_with('!'));
170
171 let normalize_glob = |glob: &str| {
172 let mut normalized_paths = Vec::new();
173
174 let anchored_glob = cargo_toml_path.parent().unwrap().join(glob);
175 let paths = glob::glob(&anchored_glob.to_string_lossy())
176 .context(format!("'{target_name}' has invalid allowlist glob format"))?;
177
178 for path in paths {
179 let path =
180 std::path::absolute(path?).context("failed to make path absolute")?;
181
182 if path.is_dir() {
183 continue;
184 }
185
186 normalized_paths.push(path);
187 }
188
189 anyhow::Ok(normalized_paths)
190 };
191
192 for glob in ignored_globs {
193 normalized_ignorelist.extend(normalize_glob(glob.strip_prefix('!').unwrap())?)
194 }
195
196 for glob in allowed_globs {
197 for path in normalize_glob(glob)? {
198 if !normalized_ignorelist.contains(&path) {
199 normalized_allowlist.push(path)
200 }
201 }
202 }
203
204 if normalized_allowlist.is_empty() {
205 anyhow::bail!("'{target_name}' has allowlist that matches no files")
206 }
207
208 targets.push(FuzzCrateTarget {
209 name: target_name.clone(),
210 allowlist: normalized_allowlist,
211 target_options: fuzz_meta
212 .target_options
213 .get(target_name)
214 .cloned()
215 .unwrap_or_default(),
216 })
217 }
218 targets
219 };
220
221 let fuzz_crate_metadata = FuzzCrateMetadata {
222 crate_name: manifest.package.as_ref().map(|x| x.name.clone()).unwrap(),
223 fuzz_dir: cargo_toml_path.parent().unwrap().into(),
224 targets,
225 };
226
227 Ok(Some(fuzz_crate_metadata))
228}
229
230pub(super) fn get_repo_fuzz_crates(
231 ctx: &crate::XtaskCtx,
232) -> anyhow::Result<Vec<FuzzCrateMetadata>> {
233 let cargo_tomls = ignore::Walk::new(&ctx.root).filter_map(|entry| match entry {
234 Ok(entry) if entry.file_name() == "Cargo.toml" => Some(entry.into_path()),
235 Err(err) => {
236 log::error!("error when walking over subdirectories: {}", err);
237 None
238 }
239 _ => None,
240 });
241
242 let mut fuzz_crates = Vec::new();
243 let mut errors = Vec::new();
244 for path in cargo_tomls {
245 match parse_fuzz_crate_toml(&path) {
246 Ok(None) => {}
247 Ok(Some(meta)) => fuzz_crates.push(meta),
248 Err(e) => errors.push(e.context(format!("in {}", path.display()))),
249 }
250 }
251
252 if !errors.is_empty() {
253 for e in &errors {
254 log::error!("{:#}", e);
255 }
256 anyhow::bail!("failed to verify in-tree fuzzers")
257 }
258
259 Ok(fuzz_crates)
260}
261
262pub(super) fn get_repo_fuzz_targets(
263 fuzz_crates: &[FuzzCrateMetadata],
264) -> anyhow::Result<BTreeMap<String, RepoFuzzTarget>> {
265 let mut fuzz_targets = BTreeMap::new();
266 for FuzzCrateMetadata {
267 fuzz_dir,
268 targets,
269 crate_name,
270 } in fuzz_crates
271 {
272 for FuzzCrateTarget {
276 name,
277 allowlist,
278 target_options,
279 } in targets
280 {
281 let existing = fuzz_targets.insert(
282 name.clone(),
283 RepoFuzzTarget {
284 crate_name: crate_name.clone(),
285 fuzz_dir: fuzz_dir.clone(),
286 allowlist: allowlist.clone(),
287 target_options: target_options.clone(),
288 },
289 );
290
291 if let Some(existing) = existing {
292 anyhow::bail!(
293 "cannot have two targets with the same name: {} (in {} and {})",
294 name,
295 fuzz_dir.display(),
296 existing.fuzz_dir.display()
297 )
298 }
299 }
300 }
301
302 Ok(fuzz_targets)
303}